
#pragma once


#include <Arduino.h>

#include "arduino_timeout_counter.hpp"

namespace {

    using ArduinoSerialType = decltype(Serial);


    // enum class transmission_state {
    //     ok,
    //     timeout,
    //     terminated,
    //     unavailable,
    // };


    // struct transmission_result {
    //     size_t count;
    //     transmission_state state;
    // };

    namespace backend_error {

        constexpr int timeout = -1;
        // constexpr int unavailable = -2;

    }  // namespace backend_error


    class ArduinoSerialBackend {
       private:
        ArduinoSerialType &_serial;

        using _BufferSizeType = decltype(_serial.available());

        scheduler_basic::ArduinoUsTimeoutCounter _rx_timeout{0};
        scheduler_basic::ArduinoUsTimeoutCounter _tx_timeout{0};

       public:
        ArduinoSerialBackend(ArduinoSerialType &serial) :
            _serial(serial) {
        }

        void begin(uint32_t baud, uint8_t config) {
            _serial.begin(baud, config);
        }

        void end() {
            _serial.end();
        }

        int rx_available() const {
            return _serial.available();
        }

        int tx_available() const {
            return _serial.availableForWrite();
        }

        void set_rx_timeout(uint32_t t) {
            _rx_timeout.set(t);
        }

        uint32_t get_rx_timeout() const {
            return _rx_timeout.get();
        }

        void set_tx_timeout(uint32_t t) {
            _tx_timeout.set(t);
        }

        uint32_t get_tx_timeout() const {
            return _tx_timeout.get();
        }

        /**
         * @brief
         *
         * @param buf
         * @param count
         * @param terminator
         * @return _BufferSizeType
         */
        _BufferSizeType read(uint8_t *buf, size_t count) {
            size_t i = 0;
            for (; i < count; i++) {
                int r = wait_and_read();
                if (r < 0) {
                    break;
                }

                buf[i] = static_cast<uint8_t>(r);
            }

            return i;
        }

        _BufferSizeType write(uint8_t *buf, size_t count) {
            size_t i = 0;

            for (; i < count; i++) {
                int r = wait_and_write(buf[i]);
                if (r < 0) {
                    break;
                }
            }

            return i;
        }


        int wait_and_read() {
            // 如果不启用超时控制，则切换为无阻塞模式，立即返回
            _rx_timeout.start();
            while (rx_available() < 1) {
                if (_rx_timeout.expired()) {
                    return backend_error::timeout;
                }
            }

            return _serial.read();
        }

        int wait_and_write(uint8_t d) {
            // 如果不启用超时控制，则切换为无阻塞模式，立即返回
            _tx_timeout.start();
            while (tx_available() < 1) {
                if (_tx_timeout.expired()) {
                    return backend_error::timeout;
                }
            }

            _serial.write(d);
            return 1;
        }
    };

}  // namespace

namespace modpash {

    /**
     * @brief 为了能用一份AduHandler 代码处理多种Pdu 类型，需要一个基类做类型擦除
     *
     */
    class PduBase {
       public:
        using MarkType = uint32_t;

       private:
        MarkType _mark;

       public:
        // PduBase(MarkType mark) : _mark(mark) {}

        /**
         * @brief 获取标识
         *
         * 可以给Pdu 对象设置记号，AduHandler 接收到回调时，先判断记号，再决定要用什么处理方式。
         * 目的是让一份AduHandler 代码能够处理RTU，ASCII，TCP 或其他扩展协议，要处理不同协议Pdu 对象的差异，
         * 只需根据mark 选择不同的代码。
         *
         * 也可以用来标记某个Pdu 对象被关联到了哪一个通信端口，比如一个PduAscii 关联到Serial0，另一个则是Serial1，
         *
         *
         * @return MarkType
         */
        MarkType get_mark() const {
            return _mark;
        }

        void set_mark(MarkType m) {
            _mark = m;
        }
    };


}  // namespace modpash


namespace modpash {

    class AduBase {
    };

}  // namespace modpash


namespace modpash {


    constexpr uint8_t half_byte_to_ascii(uint8_t d) {
        return (d < 10) ? (d + '0') : (d + 'A' - 10);
    };

    constexpr uint8_t ascii_to_half_byte(uint8_t d) {
        if (d < ':') {
            return d - '0';
        }
        else if (d > '@') {
            return d - 'A' + 10;
        }
        else {
            return 0xee;  // 随便返回一个大于0x0f 的无效数值
        }
    }

    constexpr uint8_t two_ascii_to_byte(uint8_t h, uint8_t l) {
        return (ascii_to_half_byte(h) << 4) + ascii_to_half_byte(l);
    }

    enum class pdu_error {
        none,

        read_timeout,
        no_frame,
        rx_empty,
        address_mismatch,
        too_short_frame,
        checksum_mismatch,
        invalid_code,
        new_frame_before_end,

        write_timeout,

        write_ok,

    };

    /**
     * @brief 表示PDU 的当前状态
     *
     * 与pdu_error 配合，pdu_error 用来判断发生了什么，pdu_state 用来判断pdu 状态机进行了何种自动处理。
     * 比如，如果返回了pdu_error::read_timeout，pdu_state 可能是pdu_state::session_idle，
     * 表示pdu 已经终止了数据帧的读取，接收缓冲区可能已被清空；而如果是pdu_state::reading, 表示pdu 没有终止帧读取，
     * 下一次调用read 时，将接着读取剩余的部分。
     *
     */
    enum class pdu_state {
        session_idle,

        reading,
        writing,
    };

    // constexpr size_t DefaultFrameBufferSize = 256;   // Modbus RTU 模式一帧最大长度为256 字节，包括PDU 帧头和CRC 段

    /**
     * @brief Modbus Ascii 协议收发器
     *
     * ## 模板参数
     * Backend 和AduHandler 采用模板参数注入类型，可以提供更大的灵活性，允许Backend 和AduHandler 为静态类型，
     * 此时为静态多态模式；如果Backend 和AduHandler 是个抽象类，就又变成了基于虚函数的多态。
     *
     * ##
     *
     * @tparam Backend     通信端口底层实现
     * @tparam AduHandler  应用层回调
     */
    // template<typename Backend, typename AduHandler>
    class PduAscii : PduBase {
       public:
        // TODO:
        using Backend = ArduinoSerialBackend;
        using AduHandler = AduBase;

        using BackendType = Backend;
        using AduHandlerType = AduHandler;

        static constexpr size_t ADU_OFFSET = 1;  // ADU 在一帧数据中的起始位置，指向功能码

        static constexpr size_t PDU_PREFIX_LEN = ADU_OFFSET;  // PDU 头部长度，即一字节的从机地址
        static constexpr size_t PDU_SUFFIX_LEN = 1;           // 表示PDU 尾部长度，即一字节的LRC 校验码（用ASCII 表示为两字节）

       private:
        // callback


        Backend *_serial;
        uint8_t *_buf;
        size_t _buf_len;

        size_t _adu_size;  // 写入的adu 数据尺寸，或是接收到的数据帧中ADU 部分的尺寸

        pdu_error _last_error = pdu_error::none;

        uint8_t _addr;  // 主机模式时表示目标从机的地址，从机模式时表示自身地址

        // 如果发生了传输错误，可能需要先确认上一个数据帧的状态。
        // 比如，主机向从机发送指令后没有接收到从机的响应，可能是从机根本没收到指令，也可能是从机发出的响应没被主机接收到，
        // 如果主机直接重新发送指令，某些指令可能导致从机工作异常，所以此时，主机可以查询从机接收到的上一条指令的信息，
        // 确认有没有执行上一条指令
        // PDU 对象中只含有上一条指令的校验和信息，完整的诊断信息应该还需要应用层给出

        uint8_t _last_rx_frame_checksum;  // 上一个接收到的帧的LRC 校验码
        uint8_t _last_tx_frame_checksum;  // 上一个发送出去的帧的LRC 校验码

        bool _is_server = false;


       public:
        /**
         * @brief Construct a new Pdu Ascii object
         *
         * @param backend        通信后端对象指针
         * @param frame_buffer   容纳一帧数据的缓冲区
         * @param buffer_size    缓冲区尺寸，处理Ascii 协议时，先解码再存入缓冲区，所以缓冲区尺寸和RTU 基本相同，
         *                       LRC 校验占用尾部一字节，而CRC 校验是两字节
         */
        PduAscii(Backend &backend, uint8_t *frame_buffer, size_t buffer_size) :
            _serial(&backend), _buf(frame_buffer), _buf_len(buffer_size) {

            // TODO: 如果允许一个Pdu 对象独占一路串口，就只需要在Pdu 初始化时配置一次串口模式，
            // 否则需要在每次收发数据前重新配置模式
            // 不如把这个工作交给用户，由用户自己判断配置串口模式的时机


            // TODO: 应该允许采用中断驱动Pdu 状态机和回调，比如，超时后，定时器中断主动调用Pdu 对象的成员函数timeout_notify，
            //  Pdu 将会看情况更新内部状态，或者进一步调用用户注册的回调函数
        }

        /**
         * @brief 重新设置帧缓冲区，返回原缓冲区的指针
         *
         * @param frame_buffer
         * @param buffer_size
         * @return uint8_t*
         */
        uint8_t *set_frame_buffer(uint8_t *frame_buffer, size_t buffer_size) {
            _modpash_assert_with_message(frame_buffer != nullptr, "帧缓冲区不能为空指针");
            _modpash_assert_with_message(buffer_size >= 16, "帧缓冲区大小至少为16 字节");

            uint8_t *tmp = _buf;
            _buf = frame_buffer;
            _buf_len = buffer_size;
            return tmp;
        }

        size_t frame_buffer_size() const {
            return _buf_len;
        }

        /**
         * @brief 返回缓冲区中指向ADU 起始位置的指针
         *
         * 对于RTU 和ASCII 协议，就是从机地址后、缓冲区的第二个字节
         *
         * @return uint8_t*
         */
        uint8_t *begin_adu() {
            return _buf + ADU_OFFSET;
        }

        void end_adu(size_t adu_size) {
            _adu_size = adu_size;
        }

        /**
         * @brief 返回缓冲区中可用作ADU 的数据区长度
         *
         * MODBUS 标准规定RTU 和ASCII 协议中，ADU 数据长度不超过253 字节（包括一字节功能码），
         * 但是modpash 库并不对此做限制，如果设置了缓冲区长度为1024，那么ASCII 模式下ADU 最大长度就可以为1022。
         *
         * @return size_t
         */
        size_t max_adu_size() const {
            return _buf_len - (PDU_PREFIX_LEN + PDU_SUFFIX_LEN);
        }

        size_t adu_size() const {
            return _adu_size;
        }

        void begin_client_session(uint8_t target_address) {
            _addr = target_address;
            _is_server = false;
        }

        void begin_server_session(uint8_t self_address) {
            _addr = self_address;
            _is_server = true;
        }

        void end_session() {
            // TODO:
        }

        pdu_error last_error() const {
            return _last_error;
        }

        void reset_last_error() {
            _last_error = pdu_error::none;
        }

        uint8_t rx_checksum() const {
            return _last_rx_frame_checksum;
        }

        uint8_t tx_checksum() const {
            return _last_tx_frame_checksum;
        }


       private:
        /**
         * @brief 为数据帧附加PDU 帧头和帧尾
         *
         * 添加从机地址和LRC 校验，ASCII 协议的起始、终止符不会体现在帧缓冲内，只在发送数据时添加
         *
         */
        size_t _complete_frame() {
            if (_adu_size < 1) {  // ADU 至少应该包含一字节功能码
                return 0;
            }

            // 附加PDU 帧头, 就是把从机地址写进帧缓冲
            _buf[0] = _addr;

            uint8_t lrc_sum = 0;
            size_t index = 0;

            for (; index < _adu_size + PDU_PREFIX_LEN; ++index) {
                lrc_sum += _buf[index];
            }

            lrc_sum = 0 - lrc_sum;
            _buf[index] = lrc_sum;
            _last_tx_frame_checksum = lrc_sum;
            return _adu_size + PDU_PREFIX_LEN + PDU_SUFFIX_LEN;
        }

        /**
         * @brief 读取串口输入，直到帧起始符，抛弃起始符和其之前的数据
         *
         * @return int   小于0 表示超时
         */
        int _sync_to_frame_start() {
            while (true) {
                int r = _serial->wait_and_read();

                if (r < 0 || r == ':') {
                    // TODO: RTU 协议也可以用类似的方法找到疑似的帧起始符
                    // 就是查找等于0 或从机地址的一个字节
                    return r;
                }
            }
        }


       public:
        /**
         * @brief 准备好ADU 数据后，发送整个数据帧
         *
         *
         */
        size_t write_frame() {
            size_t frame_len = _complete_frame();
            if (frame_len == 0) {
                // TODO: 回调，错误处理，状态转换
                return 0;
            }

            auto wait_and_write_2 = [this](uint8_t d1, uint8_t d2) {
                return _serial->wait_and_write(d1) && _serial->wait_and_write(d2);
            };

            // 发送起始符号
            if (_serial->wait_and_write(':') < 0) {
                _last_error = pdu_error::write_timeout;
                return 0;
            }

            uint8_t tmp_buf[2];

            // 开始一边转码一边发送
            for (size_t i = 0; i < frame_len; ++i) {
                uint8_t d = _buf[i];

                // 每个字节分成两个ASCII 码，高四位先发
                tmp_buf[0] = half_byte_to_ascii(d >> 4);
                tmp_buf[1] = half_byte_to_ascii(d & 0x0f);

                auto r = _serial->write(tmp_buf, 2);

                if (r != 2) {
                    _last_error = pdu_error::write_timeout;
                    return 0;
                }
            }

            // 发送结束符号
            if (!wait_and_write_2('\r', '\n')) {
                _last_error = pdu_error::write_timeout;
                return 0;
            }

            return frame_len;
        }


        size_t read_frame() {
            if (_serial->rx_available() < 1) {
                _last_error = pdu_error::rx_empty;
                return 0;
            }

            int r = _sync_to_frame_start();
            if (r != ':') {
                _last_error = pdu_error::no_frame;
                return 0;
            }

            size_t i = 0;
            uint8_t tmp_buf[2];
            uint8_t sum;  // 一边接收，一边计算LRC 校验和
            uint8_t d;

            while (true) {
                size_t count = _serial->read(tmp_buf, 2);

                // 去掉起始符号以后，整个ASCII 帧的长度一定是2 的倍数
                if (count < 2) {  // 读取到的没到2 字节，说明读取已经超时，丢弃这一帧
                    _last_error = pdu_error::read_timeout;
                    return 0;
                }

                // 如果在结束符号之前读到了新的开始符号，丢弃
                if (tmp_buf[0] == ':' || tmp_buf[1] == ':') {
                    _last_error = pdu_error::new_frame_before_end;
                    return 0;
                }

                if (tmp_buf[0] == '\r') {
                    // 读到了结束符号CRLF
                    // 总字节数至少为3，包括从机地址、功能码、LRC 校验码
                    // 读到的字节数小于3 时，丢弃这一帧
                    if (i < PDU_PREFIX_LEN + PDU_SUFFIX_LEN + 1) {
                        _last_error = pdu_error::too_short_frame;
                        return 0;
                    }

                    _adu_size = i - PDU_PREFIX_LEN - PDU_SUFFIX_LEN;

                    // 检查LRC，读取到的上一个字节是LRC 校验码，要从sum 中先减去
                    // 再用读取到的LRC 校验码与计算值比较
                    sum -= d;
                    sum = 0 - sum;
                    if (d != sum) {  // 校验失败，丢弃
                        _last_error = pdu_error::checksum_mismatch;
                        return 0;
                    }

                    _last_error = pdu_error::none;
                    return i;
                }

                uint8_t h = ascii_to_half_byte(tmp_buf[0]);
                uint8_t l = ascii_to_half_byte(tmp_buf[1]);

                if (h > 0xf || l > 0xf) {
                    _last_error = pdu_error::invalid_code;
                    return 0;
                }

                d = (h << 4) + l;

                if (_is_server && i == 0) {  // 第一个字节是从机地址
                    // 读到的不是发给自己的帧，也不是广播地址，丢弃
                    // 如果是主机，则不判断地址，一律接收
                    if (d != _addr && d != 0) {
                        _last_error = pdu_error::address_mismatch;
                        return 0;
                    }
                }

                _buf[i] = d;
                sum += d;
                ++i;
            }
        }
    };

}  // namespace modpash
